using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Db4objects.Db4o;
using Db4objects.Db4o.Linq;

namespace SharedObjects
{
	public static class Extension
	{
		/// <summary>
		/// Stores an EntityBase derived object in the database recursively.
		/// </summary>
		/// <param name="db"></param>
		/// <param name="sourceObject">The object to add/update.</param>
		public static void PersistEntity(this IObjectContainer db, EntityBase sourceObject)
		{
			if (sourceObject == null)
				return;

			EntityBase result = db.ConnectEntityRecursive(sourceObject);
			db.Store(result);
			//db.Commit();
			return;
		}

		//sequence of work in this method:
		//1. Traverse all properties
		//2. Every property that can be recursive (EntityBase, List<>) shall be recursed into as follows:
		//		EntityBase: Call ConnectEntityRecursive on the property value
		//		List<>: if contained type is EntityBase -> for each item call ConnectEntityRecursive AFTER db.Store'ing the List<> itself
		//				if contained type is not EntityBase -> for each item call db.Store, after Foreach db.Store List<> itself.
		//
		//Prerequisites for the database storage to work properly: All objects to be used must either be the most basic objects such as string, int etc etc, or the 
		//complex object must derive from EntityBase. Please see Libraries/SharedObjects/EntityDefinitions for how that looks
		private static EntityBase ConnectEntityRecursive(this IObjectContainer db, EntityBase unconnectedParent)
		{
			if (unconnectedParent == null)
				return null;
			if (unconnectedParent.Id == 0)
				return null;
			//fetch object from DB if exists
			EntityBase connectedParent = (from EntityBase b in db where b.Id == unconnectedParent.Id select b).FirstOrDefault();

			//if this object is not in the DB, assign the source object to this var, since the rest of the method will assign objects to the objInDB.
			//not true because the recursion works always on the unconnectedParent, since the parent will be connected last, using the property values of the 
			//unconnected parent.
			//if the parent is not existant in the database, it will be inserted automatically.
			//if(connectedParent == null)
			//    connectedParent = unconnectedParent;

			//traverse source object's properties of (base)type EntityBase
			var parentProperties = unconnectedParent.GetType().GetProperties().Where(it => it.PropertyType.IsSubclassOf(typeof(EntityBase)));
			foreach (PropertyInfo parentProperty in parentProperties)
			{
				//get the contained (unconnected) child from the source parent
				EntityBase unconnectedChild = (EntityBase)parentProperty.GetValue(unconnectedParent, null);

				//persist and connect the object
				EntityBase connectedChild = db.ConnectEntityRecursive(unconnectedChild);

				//assign the connected and persisted child to the UNCONNECTED parent (all properties of the unconnected parent will be copied to the connected parent later!).
				PropertyInfo targetPi = unconnectedParent.GetType().GetProperty(parentProperty.Name);
				targetPi.SetValue(unconnectedParent, connectedChild, null);
			}

			//now all direct references of basetype EntityBase are covered, re-connected, persisted and assigned to the connectedParent object
			//we turn to any collection that is in the parent
			parentProperties = unconnectedParent.GetType().GetProperties().Where(it => it.PropertyType.IsGenericType && it.PropertyType.GetGenericTypeDefinition() == typeof(List<>));
			foreach (PropertyInfo parentProperty in parentProperties)
			{
				IList unconnectedList = (IList)parentProperty.GetValue(unconnectedParent, null);
				if (unconnectedList == null)
					continue;

				Type d1 = typeof(List<>);
				Type itemType = unconnectedList.GetType().GetProperties()[2].PropertyType;
				if (true)//itemType == typeof(EntityBase)) //conditional removed to allow storing of lists with non-EntityBase itemtype
				{
					Type[] args = { itemType };
					var blueprint = d1.MakeGenericType(args);
					IList connectedList = (IList)Activator.CreateInstance(blueprint);
					foreach (object item in unconnectedList)
					{
						if (item is EntityBase)
							connectedList.Add(db.ConnectEntityRecursive(item as EntityBase));
						else
						{
							//db.Store(item);
							connectedList.Add(item);
						}
					}
					if (itemType != typeof(EntityBase))
						db.Store(connectedList);
					parentProperty.SetValue(unconnectedParent, connectedList, null);
				}
				//else
				//{
				//    parentProperty.SetValue(unconnectedParent, unconnectedList, null);
				//}

				//IList unconnectedList = (IList)parentProperty.GetValue(unconnectedParent, null);
				//var d1 = unconnectedList.GetType();
				//Type[] args = {typeof (EntityBase)};
				//var blueprint = d1.MakeGenericType(args);
				//IList connectedList = (IList) Activator.CreateInstance(blueprint);

				//if (unconnectedList != null) //the arraylist CAN be null, if not yet constructed for this object
				//{
				//    foreach (var item in unconnectedList)
				//    {
				//        //if the item in the ArrayList is an Entity
				//        if (item is EntityBase)
				//        {
				//            //first reconnect the entity
				//            EntityBase unconnectedItem = item as EntityBase;
				//            EntityBase connectedItem = db.PersistEntityRecursive(unconnectedItem);
				//            //then add this entity to the NEW arraylist
				//            connectedList.Add(connectedItem);
				//            continue;
				//        }
				//        //here we copy any "normal" item to the new list.
				//        connectedList.Add(item);
				//    }
				//}
				//now we assign the new arraylist to the property of the unconnected parent.
				//PropertyInfo targetPi = unconnectedParent.GetType().GetProperty(parentProperty.Name);
				//targetPi.SetValue(unconnectedParent, connectedList, null);
			}

			//at this point all child objects that might need reconnection have been processed, including lists
			//if the parent object exists in the database
			//we finally traverse all properties of the parent to copy them to the connected parent object (update)
			//otherwise
			//we simply store the unconnected parent object, since it is new (insert)
			if (connectedParent == null) //parent was not found in db
			{
				db.Store(unconnectedParent);
				//db.Commit();
				return unconnectedParent; //unconnected parent has been inserted into db, and is now connected.
			}

			parentProperties = unconnectedParent.GetType().GetProperties();
			foreach (PropertyInfo parentProperty in parentProperties)
			{
				if (parentProperty.Name == "ID") //this is hard coded, because its the only property that we dont want to update. Since this method deals with EntityBase objects only, we know that the property's name is "ID".
					continue;

				//copy the value of the parent property to the one coming from the DB.
				var value = parentProperty.GetValue(unconnectedParent, null);
				parentProperty.SetValue(connectedParent, value, null);
			}
			db.Store(connectedParent);
			return connectedParent;
		}

		public static string EncryptPassword(this string s)
		{
			SHA256Managed sha = new SHA256Managed();
			sha.Initialize();
			RNGCryptoServiceProvider random = new RNGCryptoServiceProvider();			
			byte[] salt = new byte[new Random(Environment.TickCount).Next(16,64)];
			random.GetBytes(salt);

			byte[] input = Encoding.UTF8.GetBytes(s);
			byte[] hashed = sha.ComputeHash(input);
			string output = Convert.ToBase64String(hashed);
			return output;
		}

		/// <summary>
		/// "Converts" a guid to ulong.
		/// Here's a problem:
		/// At the time of writing, db4o did not support indexing complex types (such as System.Guid), but simple types only (such as ulong)
		/// My workaround for this is - for now - to convert every Guid from 32 bytes to 8 bytes, which drastically increases collision chances of course,
		/// however it tends to work in the majority of cases (i made a little test program once to try and collide the resulting values, which i couldn't).
		/// When db4o has added complex type indexing, all the EntityBase ids can again be of type Guid instead of ulong.
		/// This is obviously not a nice workaround, but for now it works.
		/// </summary>
		public static ulong ToLong(this Guid id)
		{
#warning ###### workaround.
			return BitConverter.ToUInt64(id.ToByteArray(), 8);
		}

		public static Guid Increment(this Guid id)
		{
			byte[] bytes = id.ToByteArray();
			IncrementByteSequence(bytes, 15);
			return new Guid(bytes);
		}

		/// <summary>
		/// This method increments the values of a GUID byte sequence.
		/// It increments the byte values from back to front.
		/// There is probably a more elegant solution, perhaps using binary operators,
		/// but this will do for now.
		/// </summary>
		private static void IncrementByteSequence(byte[] seq, int index)
		{
			if(index >= seq.Count())
				throw new InvalidOperationException("Index out of range.");
			if (index < 0)
				throw new InvalidOperationException("Cannot increment this Guid any further.");

			if (seq[index] == 0xFF)
			{
				seq[index] = 0x00;
				IncrementByteSequence(seq, index - 1);
			}
			else
				seq[index]++;
		}
	}
}